{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "colab_type": "text",
        "id": "cgvP6mUf5WJs"
      },
      "source": [
        "# Time-varying Convex Optimization\n",
        "\n",
        "This notebook will provide implementation and examples from the paper [Time-varying Convex Optimization](https://arxiv.org/abs/1808.03994), Amir Ali Ahmadi and Bachir El Khadir, 2018.\n",
        "\n",
        "*  bachir009@gmail.com\n",
        "*  sindhwani@google.com"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "colab_type": "text",
        "id": "qDTiddF1Q8Iu"
      },
      "source": [
        "#### Copyright 2018 Google LLC.\n",
        "Licensed under the Apache License, Version 2.0 (the \"License\");"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 0,
      "metadata": {
        "colab": {},
        "colab_type": "code",
        "id": "KBUl8K94RZ3D"
      },
      "outputs": [],
      "source": [
        "# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
        "# you may not use this file except in compliance with the License.\n",
        "# You may obtain a copy of the License at\n",
        "#\n",
        "# https://www.apache.org/licenses/LICENSE-2.0\n",
        "#\n",
        "# Unless required by applicable law or agreed to in writing, software\n",
        "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
        "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
        "# See the License for the specific language governing permissions and\n",
        "# limitations under the License."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "colab_type": "toc",
        "id": "5WIm6YGBQAiT"
      },
      "source": [
        "\u003e[Time-varying Convex Optimization](#scrollTo=cgvP6mUf5WJs)\n",
        "\n",
        "\u003e\u003e\u003e\u003e[Copyright 2018 Google LLC.](#scrollTo=qDTiddF1Q8Iu)\n",
        "\n",
        "\u003e\u003e[Install Dependencies](#scrollTo=_xLiNJfmORvW)\n",
        "\n",
        "\u003e\u003e[Time Varying Semi-definite Programs](#scrollTo=6PuweE1NO-sZ)\n",
        "\n",
        "\u003e\u003e[Some Polynomial Tools](#scrollTo=27St0x2TO7Eu)\n",
        "\n",
        "\u003e\u003e[Examples: To Add.](#scrollTo=enYVtJrS5mCw)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "colab_type": "text",
        "id": "_xLiNJfmORvW"
      },
      "source": [
        "## Install Dependencies\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 0,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 625
        },
        "colab_type": "code",
        "id": "V9y_sVQAOWvO",
        "outputId": "125b2a62-30ee-415b-fc05-642210cc5776"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Collecting cvxpy\n",
            "  Using cached https://files.pythonhosted.org/packages/76/3c/4314c56be5b069f4d542046912d503a07c96b42c0b075ef0e32b48f8579f/cvxpy-1.0.10.tar.gz\n",
            "Collecting osqp (from cvxpy)\n",
            "  Using cached https://files.pythonhosted.org/packages/43/f2/bbeb83c0da6fd89a6d835b98d85ec76c04f39a476c065e3c99b6b709c493/osqp-0.4.1-cp36-cp36m-manylinux1_x86_64.whl\n",
            "Collecting ecos\u003e=2 (from cvxpy)\n",
            "  Using cached https://files.pythonhosted.org/packages/b6/b4/988b15513b13e8ea2eac65e97d84221ac515a735a93f046e2a2a3d7863fc/ecos-2.0.5.tar.gz\n",
            "Collecting scs\u003e=1.1.3 (from cvxpy)\n",
            "  Using cached https://files.pythonhosted.org/packages/b3/fd/6e01c4f4a69fcc6c3db130ba55572089e78e77ea8c0921a679f9da1ec04c/scs-2.0.2.tar.gz\n",
            "Collecting multiprocess (from cvxpy)\n",
            "  Using cached https://files.pythonhosted.org/packages/7a/ee/b9bf3e171f936743758ef924622d8dd00516c5532b00a1210a09bce68325/multiprocess-0.70.6.1.tar.gz\n",
            "Collecting fastcache (from cvxpy)\n",
            "  Using cached https://files.pythonhosted.org/packages/fb/98/93f2d36738868e8dd5a8dbfc918169b24658f63e5fa041fe000c22ae4f8b/fastcache-1.0.2.tar.gz\n",
            "Requirement already satisfied: six in /usr/local/lib/python3.6/dist-packages (from cvxpy) (1.11.0)\n",
            "Requirement already satisfied: toolz in /usr/local/lib/python3.6/dist-packages (from cvxpy) (0.9.0)\n",
            "Requirement already satisfied: numpy\u003e=1.14 in /usr/local/lib/python3.6/dist-packages (from cvxpy) (1.14.6)\n",
            "Requirement already satisfied: scipy\u003e=0.19 in /usr/local/lib/python3.6/dist-packages (from cvxpy) (1.1.0)\n",
            "Requirement already satisfied: future in /usr/local/lib/python3.6/dist-packages (from osqp-\u003ecvxpy) (0.16.0)\n",
            "Requirement already satisfied: dill\u003e=0.2.8.1 in /usr/local/lib/python3.6/dist-packages (from multiprocess-\u003ecvxpy) (0.2.8.2)\n",
            "Building wheels for collected packages: cvxpy, ecos, scs, multiprocess, fastcache\n",
            "  Running setup.py bdist_wheel for cvxpy ... \u001b[?25l-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008done\n",
            "\u001b[?25h  Stored in directory: /root/.cache/pip/wheels/8b/af/aa/46570716431521ee92085f317c33b2f427e27f08fe4a8a738a\n",
            "  Running setup.py bdist_wheel for ecos ... \u001b[?25l-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008done\n",
            "\u001b[?25h  Stored in directory: /root/.cache/pip/wheels/50/91/1b/568de3c087b3399b03d130e71b1fd048ec072c45f72b6b6e9a\n",
            "  Running setup.py bdist_wheel for scs ... \u001b[?25l-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008-\u0008 \u0008done\n",
            "\u001b[?25h  Stored in directory: /root/.cache/pip/wheels/ff/f0/aa/530ccd478d7d9900b4e9ef5bc5a39e895ce110bed3d3ac653e\n",
            "  Running setup.py bdist_wheel for multiprocess ... \u001b[?25l-\u0008 \u0008\\\u0008 \u0008|\u0008 \u0008/\u0008 \u0008done\n",
            "\u001b[?25h  Stored in directory: /root/.cache/pip/wheels/8b/36/e5/96614ab62baf927e9bc06889ea794a8e87552b84bb6bf65e3e\n",
            "  Running setup.py bdist_wheel for fastcache ... \u001b[?25l-\u0008 \u0008\\\u0008 \u0008done\n",
            "\u001b[?25h  Stored in directory: /root/.cache/pip/wheels/b7/90/c0/da92ac52d188d9ebca577044e89a14d0e6ff333c1bcd1ebc14\n",
            "Successfully built cvxpy ecos scs multiprocess fastcache\n",
            "Installing collected packages: osqp, ecos, scs, multiprocess, fastcache, cvxpy\n",
            "Successfully installed cvxpy-1.0.10 ecos-2.0.5 fastcache-1.0.2 multiprocess-0.70.6.1 osqp-0.4.1 scs-2.0.2\n",
            "Requirement already satisfied: sympy in /usr/local/lib/python3.6/dist-packages (1.1.1)\n",
            "Requirement already satisfied: mpmath\u003e=0.19 in /usr/local/lib/python3.6/dist-packages (from sympy) (1.0.0)\n"
          ]
        }
      ],
      "source": [
        "!pip install cvxpy\n",
        "!pip install sympy\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 0,
      "metadata": {
        "colab": {},
        "colab_type": "code",
        "id": "zCs4gdLfOHL_"
      },
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "import scipy as sp"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "colab_type": "text",
        "id": "6PuweE1NO-sZ"
      },
      "source": [
        "## Time Varying Semi-definite Programs"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "colab_type": "text",
        "id": "X0fMKhMrPiwH"
      },
      "source": [
        "The TV-SDP framework for CVXPY for imposing constraints of the form:\n",
        "\n",
        "$$A(t) \\succeq 0 \\; \\forall t \\in [0, 1],$$\n",
        "\n",
        "where $$A(t)$$ is a polynomial symmetric matrix, i.e. a symmetric matrix\n",
        "whose entries are polynomial functions of time, and $$A(t) \\succeq 0$$\n",
        "means that all the eigen values of the matrix $$A(t)$$ are nonnegative."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 0,
      "metadata": {
        "colab": {},
        "colab_type": "code",
        "id": "qHIq39X7DICz"
      },
      "outputs": [],
      "source": [
        "def _mult_poly_matrix_poly(p, mat_y):\n",
        "  \"\"\"Multiplies the polynomial matrix mat_y by the polynomial p entry-wise.\n",
        "\n",
        "  Args:\n",
        "    p: list of size d1+1 representation the polynomial sum p[i] t^i.\n",
        "    mat_y: (m, m, d2+1) tensor representing a polynomial\n",
        "      matrix Y_ij(t) = sum mat_y[i, j, k] t^k.\n",
        "\n",
        "  Returns:\n",
        "    (m, m, d1+d2+1) tensor representing the polynomial matrix p(t)*Y(t).\n",
        "  \"\"\"\n",
        "\n",
        "  mult_op = lambda q: np.convolve(p, q)\n",
        "  p_times_y = np.apply_along_axis(mult_op, 2, mat_y)\n",
        "  return p_times_y\n",
        "\n",
        "\n",
        "def _make_zero(p):\n",
        "  \"\"\"Returns the constraints p_i == 0.\n",
        "\n",
        "  Args:\n",
        "    p: list of cvxpy expressions.\n",
        "\n",
        "  Returns:\n",
        "    A list of cvxpy constraints [pi == 0 for pi in p].\n",
        "  \"\"\"\n",
        "\n",
        "  return [pi == 0 for pi in p]\n",
        "\n",
        "\n",
        "def _lambda(m, d, Q):\n",
        "  \"\"\"Returns the mxm polynomial matrix of degree d whose Gram matrix is Q.\n",
        "\n",
        "  Args:\n",
        "    m: size of the polynomial matrix to be returned.\n",
        "    d: degreen of the polynomial matrix to be returned.\n",
        "    Q: (m*d/2, m*d/2)  gram matrix of the polynomial matrix to be returned.\n",
        "\n",
        "  Returns:\n",
        "    (m, m, d+1) tensor representing the polynomial whose gram matrix is Q.\n",
        "    i.e. $$Y_ij(t) == sum_{r, s s.t. r+s == k}  Q_{y_i t^r, y_j t^s} t^k$$.\n",
        "  \"\"\"\n",
        "\n",
        "  d_2 = int(d / 2)\n",
        "\n",
        "  def y_i_j(i, j):\n",
        "    poly = list(np.zeros((d + 1, 1)))\n",
        "    for k in range(d_2 + 1):\n",
        "      for l in range(d_2 + 1):\n",
        "        poly[k + l] += Q[i + k * m, j + l * m]\n",
        "    return poly\n",
        "\n",
        "  mat_y = [[y_i_j(i, j) for j in range(m)] for i in range(m)]\n",
        "  mat_y = np.array(mat_y)\n",
        "  return mat_y\n",
        "\n",
        "\n",
        "def _alpha(m, d, Q):\n",
        "  \"\"\"Returns t*Lambda(Q) if d odd, Lambda(Q) o.w.\n",
        "\n",
        "  Args:\n",
        "    m: size of the polynomial matrix to be returned.\n",
        "    d: degreen of the polynomial matrix to be returned.\n",
        "    Q: gram matrix of the polynomial matrix.\n",
        "\n",
        "  Returns:\n",
        "    t*Lambda(Q) if d odd, Lambda(Q) o.w.\n",
        "  \"\"\"\n",
        "\n",
        "  if d % 2 == 1:\n",
        "    w1 = np.array([0, 1])  # t\n",
        "  else:\n",
        "    w1 = np.array([1])  # 1\n",
        "  mat_y = _lambda(m, d + 1 - len(w1), Q)\n",
        "  return _mult_poly_matrix_poly(w1, mat_y)\n",
        "\n",
        "\n",
        "def _beta(m, d, Q):\n",
        "  \"\"\"Returns (1-t)*Lambda(Q) if d odd, t(1-t)*Lambda(Q) o.w.\n",
        "\n",
        "  Args:\n",
        "    m: size of the polynomial matrix to be returned.\n",
        "    d: degreen of the polynomial matrix to be returned.\n",
        "    Q: gram matrix of the polynomial matrix.\n",
        "\n",
        "  Returns:\n",
        "    (1-t)*Lambda(Q) if d odd, t(1-t)*Lambda(Q) o.w.\n",
        "  \"\"\"\n",
        "\n",
        "  if d % 2 == 1:\n",
        "    w2 = np.array([1, -1])  # 1 - t\n",
        "  else:\n",
        "    w2 = np.array([0, 1, -1])  # t - t^2\n",
        "  mat_y = _lambda(m, d + 1 - len(w2), Q)\n",
        "  return _mult_poly_matrix_poly(w2, mat_y)\n",
        "\n",
        "\n",
        "def make_poly_matrix_psd_on_0_1(mat_x):\n",
        "  \"\"\"Returns the constraint X(t) psd on [0, 1].\n",
        "\n",
        "  Args:\n",
        "    mat_x: (m, m, d+1) tensor representing a mxm polynomial matrix of degree d.\n",
        "\n",
        "  Returns:\n",
        "    A list of cvxpy constraints imposing that X(t) psd on [0, 1].\n",
        "  \"\"\"\n",
        "\n",
        "  m, m2, d = len(mat_x), len(mat_x[0]), len(mat_x[0][0]) - 1\n",
        "\n",
        "  # square matrix\n",
        "  assert m == m2\n",
        "\n",
        "  # build constraints: X == alpha(Q1) + beta(Q2) with Q1, Q2 \u003e\u003e 0\n",
        "  d_2 = int(d / 2)\n",
        "  size_Q1 = m * (d_2 + 1)\n",
        "  size_Q2 = m * d_2 if d % 2 == 0 else m * (d_2 + 1)\n",
        "\n",
        "  Q1 = cvxpy.Variable((size_Q1, size_Q1))\n",
        "  Q2 = cvxpy.Variable((size_Q2, size_Q2))\n",
        "\n",
        "  diff = mat_x - _alpha(m, d, Q1) - _beta(m, d, Q2)\n",
        "  diff = diff.reshape(-1)\n",
        "\n",
        "  const = _make_zero(diff)\n",
        "  const += [Q1 \u003e\u003e 0, Q2 \u003e\u003e 0, Q1.T == Q1, Q2.T == Q2]\n",
        "\n",
        "  return const"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "colab_type": "text",
        "id": "27St0x2TO7Eu"
      },
      "source": [
        "## Some Polynomial Tools"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 0,
      "metadata": {
        "colab": {},
        "colab_type": "code",
        "id": "3wwvBAKBOk2d"
      },
      "outputs": [],
      "source": [
        "def integ_poly_0_1(p):\n",
        "  \"\"\"Return the integral of p(t) between 0 and 1.\"\"\"\n",
        "\n",
        "  return np.array(p).dot(1 / np.linspace(1, len(p), len(p)))\n",
        "\n",
        "\n",
        "def spline_regression(x, y, num_parts, deg=3, alpha=.01, smoothness=1):\n",
        "  \"\"\"Fits splines with `num_parts` to data `(x, y)`.\n",
        "\n",
        "  Finds a piecewise polynomial function `p` of degree `deg` with `num_parts`\n",
        "  pieces that minimizes the fitting error sum |y_i - p(x_i)| + alpha |p|_1.\n",
        "\n",
        "  Args:\n",
        "    x: [N] ndarray of  input data. Must be increasing.\n",
        "    y: [N] ndarray, same size as `x`.\n",
        "    num_parts: int, Number of pieces of the piecewise polynomial function `p`.\n",
        "    deg: int, degree of each polynomial piece of `p`.\n",
        "    alpha: float, Regularizer.\n",
        "    smoothness: int, the desired degree of smoothness of `p`, e.g.\n",
        "      `smoothness==0` corresponds to a continuous `p`.\n",
        "\n",
        "  Returns:\n",
        "     [num_parts, deg+1] ndarray representing the piecewise polynomial `p`.\n",
        "     Entry (i, j)  contains j^th coefficient of the i^th piece of `p`.\n",
        "  \"\"\"\n",
        "\n",
        "  # coefficients of the polynomial of p.\n",
        "  p = cvxpy.Variable((num_parts, deg + 1), name='p')\n",
        "\n",
        "  # convert to numpy format because it is easier to work with.\n",
        "  numpy_p = np.array([[p[i, j] for j in range(deg+1)] \\\n",
        "                    for i in range(num_parts)])\n",
        "\n",
        "  regularizer = alpha * cvxpy.norm(p, 1)\n",
        "\n",
        "  num_points_per_part = int(len(x) / num_parts)\n",
        "\n",
        "  smoothness_constraints = []\n",
        "\n",
        "  # cuttoff values\n",
        "  t = []\n",
        "\n",
        "  fitting_value = 0\n",
        "  # split the data into equal `num_parts` pieces\n",
        "  for i in range(num_parts):\n",
        "\n",
        "    # the part of the data that the current piece fits\n",
        "    sub_x = x[num_points_per_part * i:num_points_per_part * (i + 1)]\n",
        "    sub_y = y[num_points_per_part * i:num_points_per_part * (i + 1)]\n",
        "\n",
        "    # compute p(sub_x)\n",
        "    # pow_x = np.array([sub_x**k for k in range(deg + 1)])\n",
        "    # sub_p = polyval(sub_xnumpy_p[i, :].dot(pow_x)\n",
        "    sub_p = eval_poly_from_coefficients(numpy_p[i], sub_x)\n",
        "\n",
        "    # fitting value of the current part of p,\n",
        "    # equal to sqrt(sum |p(x_i) - y_i|^2), where the sum\n",
        "    # is over data (x_i, y_i) in the current piece.\n",
        "    fitting_value += cvxpy.norm(cvxpy.vstack(sub_p - sub_y), 1)\n",
        "\n",
        "    # glue things together by ensuring smoothness of the p at x1\n",
        "    if i \u003e 0:\n",
        "      x1 = x[num_points_per_part * i]\n",
        "      # computes the derivatives p'(x1) for the left and from the right of x1\n",
        "\n",
        "      # x_deriv is the 2D matrix  k!/(k-j)! x1^(k-j) indexed by (j, k)\n",
        "      x1_deriv = np.array(\n",
        "          [[np.prod(range(k - j, k)) * x1**(k - j)\n",
        "            for k in range(deg + 1)]\n",
        "           for j in range(smoothness + 1)]).T\n",
        "\n",
        "      p_deriv_left = numpy_p[i - 1].dot(x1_deriv)\n",
        "      p_deriv_right = numpy_p[i].dot(x1_deriv)\n",
        "\n",
        "      smoothness_constraints += [\n",
        "          cvxpy.vstack(p_deriv_left - p_deriv_right) == 0\n",
        "      ]\n",
        "      t.append(x1)\n",
        "  min_loss = cvxpy.Minimize(fitting_value + regularizer)\n",
        "  prob = cvxpy.Problem(min_loss, smoothness_constraints)\n",
        "  prob.solve(verbose=False)\n",
        "\n",
        "  return _piecewise_polynomial_as_function(p.value, t)\n",
        "\n",
        "\n",
        "def _piecewise_polynomial_as_function(p, t):\n",
        "  \"\"\"Returns the piecewise polynomial `p` as a function.\n",
        "\n",
        "  Args:\n",
        "    p: [N, d+1] array of coefficients of p.\n",
        "    t: [N] array of cuttoffs.\n",
        "\n",
        "  Returns:\n",
        "    The function f s.t. f(x) = p_i(x) if t[i] \u003c x \u003c t[i+1].\n",
        "  \"\"\"\n",
        "\n",
        "  def evaluate_p_at(x):\n",
        "    \"\"\"Returns p(x).\"\"\"\n",
        "\n",
        "    pieces = [x \u003c t[0]] + [(x \u003e= ti)  \u0026 (x \u003c ti_plusone) \\\n",
        "             for ti, ti_plusone in zip(t[:-1], t[1:])] +\\\n",
        "              [x \u003e= t[-1]]\n",
        "\n",
        "    # pylint: disable=unused-variable\n",
        "    func_list = [\n",
        "        lambda u, pi=pi: eval_poly_from_coefficients(pi, u) for pi in p\n",
        "    ]\n",
        "\n",
        "    return np.piecewise(x, pieces, func_list)\n",
        "\n",
        "  return evaluate_p_at\n",
        "\n",
        "\n",
        "def eval_poly_from_coefficients(coefficients, x):\n",
        "  \"\"\"Evaluates the polynomial whose coefficients are `coefficients` at `x`.\"\"\"\n",
        "  return coefficients.dot([x**i for i in range(len(coefficients))])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "colab_type": "text",
        "id": "enYVtJrS5mCw"
      },
      "source": [
        "## Examples: To Add."
      ]
    }
  ],
  "metadata": {
    "colab": {
      "collapsed_sections": [],
      "name": "TVSDP.ipynb",
      "provenance": [],
      "version": "0.3.2"
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
